在說明閉包之前,如果對於範圍鏈 ( Scope chain ) 還不是很明白的朋友,建議先去複習昨天的文章或稍作了解後在看本篇會比較好理解哦!
閉包的概念為,一個擁有巢狀結構的函式,使其中內層函式去操作外層函式的區域變數,此時外層函式的變數狀態就會被內層保留下來,形成一個封閉的狀態,只有內層可以存取該變數,來達到保護變內容的目的。
閉包雖然很好用,但也有可能發生佔用記憶體的情況發生,所以在使用上必須謹慎小心,如果對於閉包還想瞭解更多的同學,我在文末有附上一些資料,可以參閱。
以下就讓我們用程式碼來說明吧!
// 基礎閉包運用
var name = "金城武";
var obj = {
name: "郭富城",
getName: function() { // 建立一個巢狀結構的函式
return function() {
return this.name;
}
}
};
// 當變數指向內部函式時,閉包就會形成。
var result = obj.getName(); // 形成閉包
console.log(result());
看到這邊,請各位思考一下最後一個 console 顯示出來的會是?
答案是 金城武。
因為 function 是在全域下執行的,所以這邊的 this 指的是全域 ( windows ),顯示出來的也就會是 金城武。
但這很明顯不符合我們的需求,還記得上面簡介所說的目的嗎?
使內層的函式去操作外層函式的區域變數,讓其形成一個封閉的狀態,只有內層可以存取該變數。
所以讓我們來看一下如何解決這個需求吧
var name = "金城武";
var obj = {
name: "郭富城",
getName: function() {
var that = this;
return function() {
return that.name;
}
}
};
var result = obj.getName(); // 形成閉包
console.log(result());
這邊的答案就會是 郭富城,因為我們在內層函式中定義了 that,藉此形成了閉包綁住 this,也因為如此才能順利的找到這個物件的 name 。
如果對於 this 不太了解的同學,別緊張,後續的章節我們還會針對 this 來做介紹。
接下來我們再用幾個程式碼來加深印象吧!
function add() {
var count = 0;
return function() {
return count += 1;
}
}
var user = add(); // 形成閉包
var custom = add(); // 形成閉包
console.log("user", user()); // 1
console.log("user", user()); // 2
console.log("user", user()); // 3
console.log("user", user()); // 4
console.log("custom", custom()); // 1
console.log("custom", custom()); // 2
console.log("custom", custom()); // 3
console.log("custom", custom()); // 4
這個案例當中,我們利用了閉包來做到了記憶變數,在這邊偷偷跟各位同學說個秘密,小弟以前在開發類似的功能,還會定義 user1、user2 ... 到user999來記憶不同的變數呢,但自從知道閉包後,不僅省時省力也讓程式碼更簡潔也更好維護了呢!
閉包雖然很好用,但很容易發生佔用記憶體的狀況,那我們該如何解決這樣的情況呢?
var name = "金城武";
var obj = {
name: "郭富城",
getName: function() {
var that = this;
return function() {
return that.name;
}
}
};
var result = obj.getName(); // 當變數指向內部函式時,形成閉包
console.log(result()); // 郭富城
result = null; // 釋放閉包
是不是非常的簡單?只要將指向內部函式的變數設定為 null,即可釋放被佔用的記憶體。
這是因為 JavaScript 有所謂的回收機制,簡單來說,當一個變數已經不在被使用時,JavaScript 會將配置給該變數的記憶回收 (GC),如果有興趣想瞭解更多的同學,我在文末也會附上相關資料。
最後,我們來用一個大家日常中都遇到過的情況來結尾吧
for( var i = 0; i < 5; i++ ) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
聰明的同學們,你們認為顯示出來的會是 A or B 呢?
(A) 0, 1, 2, 3, 4
(B) 5, 5, 5, 5, 5
答案是 (B),答對的同學非常棒,已經可以按下一篇了,
答錯的同學也別氣餒,知恥而近乎勇,讓我們來了解一下為什麼會是 B 吧!
因為該函式裡頭的 console 會存取的範圍為 for迴圈 所在的範圍( 目前是全域 ),因此在 1秒、2秒、3秒 ... 5秒後執行的 console.log(i),會去取全域變數,而這時 for 迴圈早已跑完,所以 i 也早就變成了 5,因此每過一秒後,印出來的都會是 i 的值,也就是 5 。
那知道了問題點,我們該怎麼解決呢?
沒錯,就是利用閉包能記憶變數這點來解決!
// 利用閉包來解決
for( var i = 0; i < 5; i++ ) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i); // 利用立即函式來做出一個巢狀結構,並將 i 當作參數傳入函式內。
}
因此,就能依照我們的先前所預期的,會印出 (A) 0, 1, 2, 3, 4。
此題也可以使用 ES6 的 let 或 bind 方法來解決此問題,以下提供程式碼來讓同學思考一下。
// 使用 ES6 的 let
for( let i = 0; i < 5; i++ ) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
小提示, let 與 var 的作用域差異。
// 使用 bind 方法
for ( var i = 0; i < 5; i++ ) {
setTimeout(function() {
console.log(this);
}.bind(i), i * 1000);
}
小提示, bind 能影響到 this。
寫這篇文章時,不時翻閱手邊的書籍與資料,在完成後再看閉包有種更清晰的感覺,所以說,寫文章就是行!
參考資料:
Tommy - 深入 JavaScript 核心課程
MDN - 閉包
MDN - 記憶體管理
MDN - Bind